About SceneGraph

0x1 什么是SceneGraph

渲染引擎需要解决的问题是如何把需要绘制的各种对象高效地组织起来并绘制出来,这其中很重要的概念是SceneGraph。目前各种渲染引擎包括2D UI引擎到3D引擎都实现了类似的概念。
其实SceneGraph应该称为SceneTree更合适,因为其中的数据组织一般是采用Tree的形式。

qt中采用的SceneGraph介绍如下
https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html

cocos2d也有类似的概念如下
https://docs.cocos2d-x.org/cocos2d-x/v4/en/basic_concepts/scene.html

ogre中对SceneGraph的介绍如下
https://ogrecave.github.io/ogre/api/latest/_the-_core-_objects.html

0x2 SceneGraph包括哪些功能

典型的SceneGraph包括了SceneManager,RenderManager,ResourceManager, Camera, RenderTarget,Animation, Particle子模块,下面来简单介绍一下这些概念。

SceneManager提供对场景图的组织和管理,具体可以采用二叉分割树,八叉树等。
RenderManager提供了绘制相关操作,也就是通常所说的各种Backend,如OpenGL,Directx,Metal,Vulkan等。
ResourceManager提供了对资源的管理,包括Material, Mesh等。
Camera提供了在SceneGraph中模拟眼睛的功能。
Viewer是在SceneGraph遨游时的眼睛,通过变化Camera的位置,我们可以看到SceneGraph中不同的风景。其中提供lookAt()类似的函数来指定场景中眼睛的位置。

0x3 SceneGraph的设计

0x31 数据结构设计

渲染引擎一般采用场景节点和场景内容分离的机制,也就是说场景内容作为场景节点的成员变量,而不是把场景内容作为场景节点的子类。场景节点一般包含了类似位置,旋转等信息,场景内容指需要绘制的Mesh和渲染属性等。
另外一种实现方式是把场景内容作为场景节点的子类,这种实现方式把场景内容和场景节点耦合在一起,不利于添加新的场景内容的支持。
场景节点和场景内容分离机制带来了SceneGraph中数据松耦合的好处,场景内容添加到一个场景节点中,也可以从一个场景节点中移除。SceneManager控制的是场景节点,不需要关心具体的场景内容。我们可以精心设计场景节点的接口,这样可以保持SceneManager对场景节点的灵活控制。

下面是典型的SceneGraph数据结构图。

Data_Structure

SceneNode表示场景节点。
Object表示场景内容。一个Object可以包括多个RenderObject,如房间可以表示为一个Object,房间里的一张桌子是RenderObject。
RenderObject表示可被渲染的内容。RenderObject需要通过Mesh和Material来绘制。如前所述,一张桌子是RenderObject,然后我们需要知道桌子的Mesh,需要知道如何设置Mesh的渲染属性如shader,color等。

0x32 场景图管理器

场景图管理器实现的功能有,
第一类是数据对象管理相关的功能,包括如下功能。

  1. 创建/删除SceneNode。

  2. 创建/删除Camera。

  3. 创建/删除各种Light。

  4. 创建/删除Object,前面提到Object作为场景内容节点挂接到SceneNode上。

  5. 创建删除各种绘制对象,设置各种绘制属性。

  6. 创建/删除ParticleSystem。

  7. 创建/删除AnimationSystem。

另外是渲染流程相关功能,包括如下,

  1. 查找当前Camera可见的渲染Object。

  2. 渲染前面查找到的渲染Object

场景图管理器的结构图如下所示,
SceneManager

Core下面的SceneManager提供了基本的框架和系统中其他模块交互,对应于具体的SceneManager而言,如上图所示的BspScenemanager和OctTreeSceneManager,其核心功能是解决如何高效地查找到当前Camera可见范围内的渲染Object,因为场景图中包括了很多数据,如在关卡游戏中,场景图中包括了很多游戏关卡,如果都去渲染,性能可能很差,所以通过SceneManager快速找到Camera可见范围内的渲染Object,然后只需要渲染这些Object。

0x33 渲染管理器

渲染管理器的结构图如下所示,
RenderManager

渲染管理器也称为渲染的后端,这里来谈一下如何设计渲染管理器的接口使特定后端的代码量最少。也就是说如何把功能尽量放在Core中,Backend只保留特定平台相关的实现,这部分也是比较各种渲染引擎的跨平台技术做的好坏的评价标准之一。

RenderManager需要抽象出下面的模块作为通用的接口,这些是所有Backend都具备的特性,是公共的模块。在具体的Backend模块中中去继承这些接口从而实现各个Backend的对接。

  1. Texture操作接口。

  2. HardwareBuffer操作接口,包括Index bufer/Vertex buffer/Uniform buffer/Texture buffer等。

  3. Shader/Program操作接口。

  4. DrawCall操作接口。

0x4 渲染流程设计及优化

0x41 数据准备

绘制内容的设置
绘制内容包括各种资源,如Mesh,Skelton,Shader等。他们通过各种ReaourceManager加载进来。

绘制坐标的设置
如果SceneNode存在父子关系,有两种方法来设置绘制坐标,第一种是在生成绘制内容的时候,根据父节点的坐标计算好当前节点的坐标,然后设置到SceneNode中。另外一种方法是给SceneNode设置一个TransformNode作为SceneNode的成员,这个TransformNode是计算好的本地坐标系坐标(没有考虑父子关系),这样在包括所有SceneNode的tree创建好了以后,再去从根节点开始遍历这颗树,根据父子关系,计算出每个SceneNode的世界坐标。具体实现中推荐采用第一种方法,因为如果SceneNode的层次太深,递归遍历节点的时候可能会出现栈溢出。

具有父子关系的SceneNode结构图如下所示。

SceneNode_Strucutre

0x42 数据绘制

场景绘制的完整流程如下,

Rendering_Process

  1. 首先是设置好场景数据,这些场景数据可以是通过离线工具如Blender,由美工来制作完成并导出,然后再加载进来。渲染引擎也可以提供在线实时生成场景数据的功能,如生成矩形,球形,立方体等。设置好的场景数据封装成RenderObject,再由RenderObject组成Object。

  2. 然后是需要把场景数据挂接到场景图的SceneNode中,这样场景图管理器就拥有了场景数据了,可以根据视点在场景图中执行查询等操作。

  3. 然后开始根据Camera的位置找到可见范围内的RenderObject,这个也就是前面提到的SceneManager发挥作用的地方,可以简单地称为Culling操作。

  4. 找到了需要渲染的RenderObject集合,可以对这些RenderObject的渲染顺序进行Bactch优化,如果两个RenderObject需要的渲染Maerial是一样地,可以合并成一个drawcall,这种优化方式我们称为动态Batch。如果由美工在素材制作阶段对场景数据的组织进行优化,把能用一个drawcall进行渲染的物体合并起来,这种优化被称为静态Batch。

  5. 优化好了以后就开始调用具体的Backend进行渲染了。

0x43 流程优化

如下图所示,可以把渲染过程划分成两个线程并行处理,这样在数据加载阶段可以做上一帧的绘制动作,等数据加载完成再同步给渲染线程。

Pipeline_Optimization

另外一个优化是资源的异步加载,如果前面的Load Thread同时要响应用户操作,如果长时间在运行,会造成UI卡死,这个时候可以通过开启异步线程来处理。

0x5 SceneGraph的发展方向思考

SceneGraph技术作为各种渲染引擎的根基,目前是很完善的技术了。包括光线跟踪渲染引擎,其中也包含了SceneGraph的实现。如果要考虑未来方向,是否可以和其他方向结合,如人工智能等?搞出一套智能的渲染引擎。如是否可以用CNN来加速特大场景图的可见渲染物体查询?